1 /*
2 Copyright: Marcelo S. N. Mancini (Hipreme|MrcSnm), 2018 - 2021
3 License:   [https://creativecommons.org/licenses/by/4.0/|CC BY-4.0 License].
4 Authors: Marcelo S. N. Mancini
5 
6 	Copyright Marcelo S. N. Mancini 2018 - 2021.
7 Distributed under the CC BY-4.0 License.
8    (See accompanying file LICENSE.txt or copy at
9 	https://creativecommons.org/licenses/by/4.0/
10 */
11 module hip.font.ttf;
12 import hip.api.data.font;
13 
14 immutable dstring defaultCharset = " \náéíóúãñçabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\\|'\"`/*-+,.;:´_=!@#$%&()[]{}~^?";
15 
16 version = HipArsdFont;
17 
18 
19 private uint nextPowerOfTwo(uint number)
20 {
21     ulong value = 1;
22     while(value < number)
23     {
24         value <<= 1;
25     }
26     return cast(uint)value;
27 }
28 
29 private int round(float f)
30 {
31     return cast(int)(f+0.5);
32 }
33 
34 class HipNullFont : HipFont
35 {
36     string path;
37     this(){}
38     this(string path, uint fontSize = 32){}
39     override uint getHeight() const { return 0; }
40 
41     /**
42     *   This will cause a full load of the .ttf file, image generation and GPU upload. Should only be used
43     *   If you don't care about async
44     */
45     bool loadFromMemory(in ubyte[] data){return false;}
46     bool partialLoad(in ubyte[] data, out ubyte[] rawImage){return false;}
47     bool loadTexture(ubyte[] rawImage){return false;}
48     override int getKerning(dchar current, dchar next) const{return 0;}
49     override int getKerning(const(HipFontChar)* current, const(HipFontChar)* next) const{return 0;}
50     override HipFont getFontWithSize(uint size){return new HipNullFont();}
51 }
52 
53 
54 version(HipArsdFont)
55 /**
56 *   Check the unicode table: https://unicode-table.com/en/blocks/
57 *   There is a lot of character ranges that defines a set of characters in a language, such as:
58 *   0000—007F Basic Latin
59 *   0080—00FF Latin-1 Supplement
60 *   0100—017F Latin Extended-A
61 *   0180—024F Latin Extended-B
62 *   Maybe it will prove more useful than having a default charset
63 */
64 class HipArsd_TTF_Font : HipFont
65 {
66     import arsd.ttf;
67     protected float fontScale;
68     protected TtfFont font;
69     string path;
70     protected uint fontSize = 32;
71     protected uint _textureWidth, _textureHeight;
72     protected Hip_TTF_Font mainInstance;
73     protected Hip_TTF_Font[] clones;
74 
75     this(string path, uint fontSize = 32)
76     {
77         this.path = path;
78         this.fontSize = fontSize;
79     }
80     override uint getHeight() const { return fontSize; }
81     /**
82     *   This will cause a full load of the .ttf file, image generation and GPU upload. Should only be used
83     *   If you don't care about async
84     */
85     bool loadFromMemory(in ubyte[] data)
86     {
87         if(data == null || data.length == 0)
88             return false;
89         try{font = TtfFont(data);}
90         catch(Exception e){return false;}
91         return loadTexture(
92             generateImage(fontSize, _textureWidth, _textureHeight)
93         );
94     }
95 
96     bool partialLoad(in ubyte[] data, out ubyte[] rawImage)
97     {
98         font = TtfFont(data);
99         rawImage = generateImage(fontSize, _textureWidth, _textureHeight);
100         return true;
101     }
102 
103     override int getKerning(const(HipFontChar)* current, const(HipFontChar)* next) const
104     {
105         return cast(int)(fontScale*stbtt_GetGlyphKernAdvance(cast(stbtt_fontinfo*)&font.font, current.glyphIndex, next.glyphIndex));
106     }
107     override int getKerning(dchar current, dchar next) const
108     {
109         return cast(int)(fontScale*stbtt_GetCodepointKernAdvance(cast(stbtt_fontinfo*)&font.font, int(current), int(next)));
110     }
111 
112 
113     bool loadTexture(ubyte[] rawImage)
114     {
115         assert(rawImage !is null, "Must first generate a texture before uploading to GPU");
116         import hip.assets.image;
117         import hip.assets.texture;
118         import hip.error.handler;
119         Image img = new Image();
120         img.loadRaw(rawImage, _textureWidth, _textureHeight, 1);
121         HipTexture t = new HipTexture(null);
122 
123         bool ret = t.load(img);
124         ErrorHandler.assertErrorMessage(ret, "Loading TTF", "Could not create texture for TTF");
125         texture = t;
126         import core.memory;
127         GC.free(rawImage.ptr);
128         return ret;
129     }
130 
131     /**
132     * This function returns a new font using the same data file, with a new size.
133     * The font data will reference to this same one
134     */
135     override HipFont getFontWithSize(uint size)
136     {
137         Hip_TTF_Font ret = new Hip_TTF_Font(this.path, size);
138         ret.font = cast(TtfFont)this.font;
139         ret.mainInstance = cast(Hip_TTF_Font)(mainInstance is null ? this : mainInstance);
140         if(mainInstance)
141             mainInstance.clones~= ret;
142         else
143             clones~= ret;
144 
145         if(!ret.loadTexture(ret.generateImage(size, ret._textureWidth, ret._textureHeight)))
146             return null;
147 
148         return cast(HipFont)ret;
149     }
150 
151     protected RenderizedChar renderCharacter(dchar ch, int size, float shift_x = 0.0, float shift_y = 0.0)
152     {
153         RenderizedChar rch;
154         rch.ch = ch;
155         rch.data = font.renderCharacter(ch, size, rch.width, rch.height, shift_x, shift_y);
156         return rch;
157     }
158     /**
159     *   I'm no good packer. The image will be at least 2048xMinPowOf2
160     */
161     protected ubyte[] generateImage(int size, out uint width, out uint height, dstring charset = defaultCharset)
162     {
163         if(charset.length == 0)
164             return null;
165         scope RenderizedChar[] fontChars = new RenderizedChar[charset.length]; //TODO: USe that as it is more optimised
166         scope(exit)
167         {
168             foreach(ch; fontChars)
169                 ch.dispose();
170             import core.memory;
171             GC.free(fontChars.ptr);
172         }
173 
174         uint avgWidth = 0;
175         uint avgHeight = 0;
176         size_t i = 0;
177         foreach(dc; charset)
178         {
179             RenderizedChar rc = renderCharacter(dc, size);
180             avgWidth+= rc.width;
181             avgHeight+= rc.height;
182             fontChars[i++] = rc;
183         }
184         //Add as an error (pixel bleeding)
185         avgWidth = cast(uint)(avgWidth / charset.length) + 2;
186         avgHeight = cast(uint)(avgHeight / charset.length) + 2;
187         enum hSpacing = 1;
188         enum vSpacing = 1;
189         float x = 1;
190         float y = 0;
191         float optY = 0;
192         float scale = stbtt_ScaleForPixelHeight(&font.font, size);
193 
194         //Setting details
195         fontScale = scale;
196 
197         int ascent, descent, lineGap;
198         stbtt_GetFontVMetrics(&font.font, &ascent, &descent, &lineGap);
199 
200 
201         lineBreakHeight = cast(uint)(int(ascent - descent + lineGap) * scale);
202 
203         //First guarantee the big size
204         import core.math:sqrt;
205         uint sqrtOfCharset = cast(uint)sqrt(cast(float)charset.length) + 1;
206         uint imageWidth = avgWidth * sqrtOfCharset;
207         uint imageHeight = avgHeight * sqrtOfCharset;
208         imageHeight = nextPowerOfTwo(imageHeight);
209         imageWidth = nextPowerOfTwo(imageWidth);
210 
211         width = imageWidth;
212         height = imageHeight;
213 
214         ubyte[] image = new ubyte[](imageWidth*imageHeight);
215 
216         int largestHeightInRow = 0;
217         import hip.util.algorithm;
218 
219         foreach(fontCh; quicksort(fontChars, (RenderizedChar a, RenderizedChar b) => a.height > b.height))
220         {
221             int g = stbtt_FindGlyphIndex(&font.font, fontCh.ch);
222             int xAdvance, xOffset, yOffset, lsb;
223             int x1, y1;
224             stbtt_GetGlyphHMetrics(&font.font, g, &xAdvance, &lsb);
225             stbtt_GetGlyphBitmapBox(&font.font, g, scale,scale, &xOffset,&yOffset,&x1,&y1);
226             if(fontCh.ch == ' ')
227             {
228                 int space_x0, space_x1;
229                 if(fontCh.width == 0)
230                 {
231                     stbtt_GetCodepointBitmapBox(&font.font, int('n'), scale,scale, &space_x0, null, &space_x1, null);
232                     spaceWidth = space_x1 - space_x0;
233                 }
234                 else
235                     spaceWidth = fontCh.width;
236             }
237 
238             if(x + fontCh.width + hSpacing > imageWidth)
239             {
240                 x = hSpacing;
241                 y+= largestHeightInRow + vSpacing;
242                 largestHeightInRow = 0;
243             }
244 
245 
246             characters[fontCh.ch] = HipFontChar(fontCh.ch, cast(int)x, cast(int)y, fontCh.width, fontCh.height,
247 
248                 xOffset, yOffset, round(xAdvance*scale), 0, 0,
249                 cast(float)x/imageWidth, cast(float)y/imageHeight,
250                 cast(float)fontCh.width/imageWidth, cast(float)fontCh.height/imageHeight,
251                 g
252             );
253             fontCh.blitToImage(image, cast(int)(x), cast(int)(y), imageWidth, imageHeight);
254             x+= fontCh.width + hSpacing;
255 
256             if(fontCh.height > largestHeightInRow)
257                 largestHeightInRow = fontCh.height;
258         }
259         return image;
260     }
261 
262 }
263 
264 version(HipNullFont)
265     alias Hip_TTF_Font = HipNullFont;
266 else version(HipArsdFont)
267     alias Hip_TTF_Font = HipArsd_TTF_Font;
268 
269 
270 private struct RenderizedChar
271 {
272     dchar ch;
273     int size;
274     int width;
275     int height;
276 
277     ubyte[] data;
278 
279     void blitToImage(ref ubyte[] texture, int startX, int startY, int textureWidth, int textureHeight)
280     {
281         assert(startX + width < textureWidth, "Out of X boundaries");
282         for(size_t i = 0; i < height; i++)
283         {
284             size_t pos = (startY+i)*textureWidth + startX;
285             assert(startY + i < textureHeight, "Out of Y boundaries");
286             texture[pos..pos+width] = data[i*width..(i+1)*width];
287         }
288     }
289 
290     void dispose()
291     {
292         version(HipArsdFont)
293         {
294             if(data.ptr != null)
295             {
296                 import arsd.ttf;
297                 stbtt_FreeBitmap(data.ptr, null);
298             }
299         }
300         data = null;
301     }
302 }